﻿using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;

namespace Leow.SelectableCanvas
{
    public class DragAndResizeAdorner : Adorner
    {
        private const double ThumbBorderThickness = 2;

        internal static readonly Size ThumbSize = new Size(8, 8);

        private readonly Thumb _border;
        private readonly Thumb _bottomLeftThumb;
        private readonly Thumb _bottomRightThumb;
        private readonly Thumb _bottomThumb;
        private readonly Thumb _leftThumb;
        private readonly Thumb _rightThumb;
        private readonly Thumb _topLeftThumb;
        private readonly Thumb _topRightThumb;
        private readonly Thumb _topThumb;
        private readonly VisualCollection _visualChildren;

        public delegate void CustomPositionChangedEventHandler(object sender, CustomPositionChangedEventArgs e);
        public delegate void CustomSizeChangedEventHandler(object sender, CustomSizeChangedEventArgs e);

        public event CustomPositionChangedEventHandler PositionChanged;
        public event CustomPositionChangedEventHandler PreviewPositionChanged;
        public event CustomSizeChangedEventHandler PreviewSizeChanged;
        public new event CustomSizeChangedEventHandler SizeChanged;

        public DragAndResizeAdorner(UIElement adornedElement)
            : base(adornedElement)
        {
            _visualChildren = new VisualCollection(this);

            _border = new HatchedBorderThumb
            {
                BorderBrush = Brushes.Black,
                BorderThickness = new Thickness(
                    ThumbSize.Width,
                    ThumbSize.Height,
                    ThumbSize.Width,
                    ThumbSize.Height),
                Cursor = Cursors.SizeAll
            };
            _border.DragDelta += BorderDragDeltaEventHandler;
            _visualChildren.Add(_border);

            _leftThumb = CreateThumb(
                Cursors.SizeWE,
                LeftThumbDragDeltaEventHandler);
            _topThumb = CreateThumb(
                Cursors.SizeNS,
                TopThumbDragDeltaEventHandler);
            _rightThumb = CreateThumb(
                Cursors.SizeWE,
                RightThumbDragDeltaEventHandler);
            _bottomThumb = CreateThumb(
                Cursors.SizeNS,
                BottomThumbDragDeltaEventHandler);

            _topLeftThumb = CreateThumb(
                Cursors.SizeNWSE,
                TopLeftThumbDragDeltaEventHandler);
            _topRightThumb = CreateThumb(
                Cursors.SizeNESW,
                TopRightThumbDragDeltaEventHandler);
            _bottomRightThumb = CreateThumb(
                Cursors.SizeNWSE,
                BottomRightThumbDragDeltaEventHandler);
            _bottomLeftThumb = CreateThumb(
                Cursors.SizeNESW,
                BottomLeftThumbDragDeltaEventHandler);
        }

        protected override int VisualChildrenCount
        {
            get
            {
                return _visualChildren.Count;
            }
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            var adornedElement = AdornedElement as FrameworkElement;
            if (adornedElement != null)
            {
                var borderRect = new Rect(
                    new Point(-ThumbSize.Width, -ThumbSize.Height),
                    new Size(adornedElement.ActualWidth + ThumbSize.Width * 2, adornedElement.ActualHeight + ThumbSize.Height * 2));
                _border.Arrange(borderRect);

                var leftThumbRect = new Rect(
                    new Point(-ThumbSize.Width, adornedElement.ActualHeight / 2 - ThumbSize.Height / 2),
                    ThumbSize);
                var topLeftThumbRect = new Rect(
                    new Point(-ThumbSize.Width, -ThumbSize.Height),
                    ThumbSize);
                var topThumbRect = new Rect(
                    new Point(adornedElement.ActualWidth / 2 - ThumbSize.Width / 2, -ThumbSize.Height),
                    ThumbSize);
                var topRightThumbRect = new Rect(
                    new Point(adornedElement.ActualWidth, -ThumbSize.Height),
                    ThumbSize);
                var rightThumbRect = new Rect(
                    new Point(adornedElement.ActualWidth, adornedElement.ActualHeight / 2 - ThumbSize.Height / 2),
                    ThumbSize);
                var bottomRightThumbRect = new Rect(
                    new Point(adornedElement.ActualWidth, adornedElement.ActualHeight),
                    ThumbSize);
                var bottomThumbRect = new Rect(
                    new Point(adornedElement.ActualWidth / 2 - ThumbSize.Width / 2, adornedElement.ActualHeight),
                    ThumbSize);
                var bottomLeftThumbRect = new Rect(
                    new Point(-ThumbSize.Width, adornedElement.ActualHeight),
                    ThumbSize);

                _leftThumb.Arrange(leftThumbRect);
                _topLeftThumb.Arrange(topLeftThumbRect);
                _topThumb.Arrange(topThumbRect);
                _topRightThumb.Arrange(topRightThumbRect);
                _rightThumb.Arrange(rightThumbRect);
                _bottomRightThumb.Arrange(bottomRightThumbRect);
                _bottomThumb.Arrange(bottomThumbRect);
                _bottomLeftThumb.Arrange(bottomLeftThumbRect);
            }

            return finalSize;
        }

        protected override Visual GetVisualChild(int index)
        {
            return _visualChildren[index];
        }

        private Thumb CreateThumb(
            Cursor cursor,
            DragDeltaEventHandler dragDeltaEventHandler)
        {
            var thumb = new RectangleThumb
                            {
                                Width = ThumbSize.Width,
                                Height = ThumbSize.Height,
                                Cursor = cursor,
                                BorderThickness = new Thickness(ThumbBorderThickness)
                            };
            thumb.DragDelta += dragDeltaEventHandler;
            _visualChildren.Add(thumb);
            return thumb;
        }

        private void MoveElement(
            FrameworkElement element,
            double xOffset,
            double yOffset)
        {
            if (xOffset == 0 && yOffset == 0)
            {
                return;
            }

            var oldPosition = new Point(
                Canvas.GetLeft(element),
                Canvas.GetTop(element));
            var newPosition = new Point(
                oldPosition.X + xOffset,
                oldPosition.Y + yOffset);

            var eventArgs = new CustomPositionChangedEventArgs(
                oldPosition,
                newPosition);

            if (PreviewPositionChanged != null)
            {
                PreviewPositionChanged(this, eventArgs);
                if (eventArgs.Handled)
                {
                    return;
                }
            }

            Canvas.SetLeft(element, newPosition.X);
            Canvas.SetTop(element, newPosition.Y);

            if (PositionChanged != null)
            {
                PositionChanged(this, eventArgs);
            }
        }

        private void ResizeElement(
            FrameworkElement element,
            double widthChange,
            double heightChange,
            Action postWidthChangedOp = null,
            Action postHeightChangedOp = null)
        {
            double newWidth = element.Width + widthChange;
            double newHeight = element.Height + heightChange;

            bool canWidthChange
                = widthChange != 0
                && newWidth > element.GetActualMinWidth()
                && newWidth < element.MaxWidth;
            bool canHeightChange
                = heightChange != 0
                && newHeight > element.GetActualMinHeight()
                && newHeight < element.MaxHeight;

            if (!canWidthChange && !canHeightChange)
            {
                return;
            }

            var oldSize = new Size(element.Width, element.Height);
            var newSize = new Size(
                canWidthChange ? newWidth : oldSize.Width,
                canHeightChange ? newHeight : oldSize.Height);

            var eventArgs = new CustomSizeChangedEventArgs(oldSize, newSize);

            if (PreviewSizeChanged != null)
            {
                PreviewSizeChanged(this, eventArgs);
                if (eventArgs.Handled)
                {
                    return;
                }
            }

            if (canWidthChange)
            {
                element.Width = newWidth;
                if (postWidthChangedOp != null)
                {
                    postWidthChangedOp.Invoke();
                }
            }

            if (canHeightChange)
            {
                element.Height = newHeight;
                if (postHeightChangedOp != null)
                {
                    postHeightChangedOp.Invoke();
                }
            }

            if (SizeChanged != null)
            {
                SizeChanged(this, eventArgs);
            }
        }

        private void BorderDragDeltaEventHandler(
            object sender,
            DragDeltaEventArgs e)
        {
            var adornedElement = AdornedElement as FrameworkElement;
            if (adornedElement == null)
            {
                return;
            }

            MoveElement(adornedElement, e.HorizontalChange, e.VerticalChange);
        }

        private void BottomLeftThumbDragDeltaEventHandler(
            object sender,
            DragDeltaEventArgs e)
        {
            var adornedElement = AdornedElement as FrameworkElement;
            if (adornedElement == null)
            {
                return;
            }

            adornedElement
                .EnsureElementHasSize();

            ResizeElement(
                adornedElement,
                -e.HorizontalChange,
                e.VerticalChange,
                () => MoveElement(adornedElement, e.HorizontalChange, 0));
        }

        private void BottomThumbDragDeltaEventHandler(
            object sender,
            DragDeltaEventArgs e)
        {
            var adornedElement = AdornedElement as FrameworkElement;
            if (adornedElement == null)
            {
                return;
            }

            adornedElement
                .EnsureElementHasHeight();

            ResizeElement(adornedElement, 0, e.VerticalChange);
        }

        private void BottomRightThumbDragDeltaEventHandler(
            object sender,
            DragDeltaEventArgs e)
        {
            var adornedElement = AdornedElement as FrameworkElement;
            if (adornedElement == null)
            {
                return;
            }

            adornedElement
                .EnsureElementHasSize();

            ResizeElement(adornedElement, e.HorizontalChange, e.VerticalChange);
        }

        private void LeftThumbDragDeltaEventHandler(
            object sender,
            DragDeltaEventArgs e)
        {
            var adornedElement = AdornedElement as FrameworkElement;
            if (adornedElement == null)
            {
                return;
            }

            adornedElement
                .EnsureElementHasWidth();

            ResizeElement(
                adornedElement,
                -e.HorizontalChange,
                0,
                () => MoveElement(adornedElement, e.HorizontalChange, 0));
        }

        private void RightThumbDragDeltaEventHandler(
            object sender,
            DragDeltaEventArgs e)
        {
            var adornedElement = AdornedElement as FrameworkElement;
            if (adornedElement == null)
            {
                return;
            }

            adornedElement
                .EnsureElementHasWidth();

            ResizeElement(adornedElement, e.HorizontalChange, 0);
        }

        private void TopLeftThumbDragDeltaEventHandler(
            object sender,
            DragDeltaEventArgs e)
        {
            var adornedElement = AdornedElement as FrameworkElement;
            if (adornedElement == null)
            {
                return;
            }

            adornedElement
                .EnsureElementHasSize();

            ResizeElement(
                adornedElement,
                -e.HorizontalChange,
                -e.VerticalChange,
                () => MoveElement(adornedElement, e.HorizontalChange, 0),
                () => MoveElement(adornedElement, 0, e.VerticalChange));
        }

        private void TopRightThumbDragDeltaEventHandler(
            object sender,
            DragDeltaEventArgs e)
        {
            var adornedElement = AdornedElement as FrameworkElement;
            if (adornedElement == null)
            {
                return;
            }

            adornedElement
                .EnsureElementHasSize();

            ResizeElement(
                adornedElement,
                e.HorizontalChange,
                -e.VerticalChange,
                null,
                () => MoveElement(adornedElement, 0, e.VerticalChange));
        }

        private void TopThumbDragDeltaEventHandler(
            object sender,
            DragDeltaEventArgs e)
        {
            var adornedElement = AdornedElement as FrameworkElement;
            if (adornedElement == null)
            {
                return;
            }

            adornedElement
                .EnsureElementHasHeight();

            ResizeElement(
                adornedElement,
                0,
                -e.VerticalChange,
                null,
                () => MoveElement(adornedElement, 0, e.VerticalChange));
        }
    }
}
